package com.baroxtech.midisynthesizer;

import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.media.AudioManager;
import android.media.midi.MidiDevice;
import android.media.midi.MidiDeviceInfo;
import android.media.midi.MidiDeviceStatus;
import android.media.midi.MidiInputPort;
import android.media.midi.MidiManager;
import android.os.Build;
import android.util.Log;

import java.io.IOException;
import java.util.Arrays;

import static android.media.midi.MidiManager.*;

public class MidiSynthesizer implements AutoCloseable {
    // 原生对象指针
    private long _nativePtr;
    private MidiDevice _midiDevice;
    private MidiManager _midiManager;
    private MidiInputPort _midiInputPort;
    private static final String TAG = "MidiSynthesizer";

    private MidiManager.DeviceCallback _midiDeviceCallback = new MidiManager.DeviceCallback() {
        @Override
        public void onDeviceAdded(MidiDeviceInfo info) {
            tryConnectMidiDevice(info);
        }

        @Override
        public void onDeviceRemoved(MidiDeviceInfo info) {
            tryDisconnectMidiDevice(info);
        }

        @Override
        public void onDeviceStatusChanged(MidiDeviceStatus status) {
        }
    };

    static {
        System.loadLibrary(BuildConfig.LIB_NAME);
    }

    @Override
    public void close() {
        if (_nativePtr != 0) {
            releaseNativePtr(_nativePtr);
            _nativePtr = 0;
        }
        if (_midiManager != null) {
            _midiManager.unregisterDeviceCallback(_midiDeviceCallback);
            tryDisconnectMidiDevice(null);
        }
    }

    /**
     * @param soundFont 在 assets 中音频字体文件的路径
     * @param gain      音量增益。有效值为 [0, 5]
     * @throws IllegalArgumentException
     */
    public MidiSynthesizer(final Context context, final String soundFont, float gain) throws Exception {
        if (gain < 0 || gain > 5) throw new IllegalArgumentException("gain 的值无效");
        AssetManager am = context.getAssets();
        try {
            if (am.list(soundFont) == null) throw new IllegalArgumentException("指定 sound font 不存在");
        } catch (IOException e) {
            throw e;
        }
        _nativePtr = createNativePtr(am, soundFont, getBestSampleRate(context), gain);
        if (_nativePtr == 0) throw new IllegalArgumentException("创建 MidiSynthesizer 的时候出现参数异常");

        // 初始化 midi 物理设备
        boolean isSupport = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_MIDI);
        Log.d(TAG, "设备" + (isSupport ? "" : "不") + "支持 midi 特性");
        if (isSupport) {
            initMidiDevice(context);
        }
    }

    /**
     * 按下按键
     *
     * @param chan  midi 通道。0 - 15 不知道可以传 0
     * @param pitch 音高
     * @param vel   力度 0 - 127
     */
    public void noteon(int chan, int pitch, int vel) {
        synth((byte) (0x90 | chan), (byte) pitch, (byte) vel);
    }

    /**
     * 抬起按键。可以理解为停止某个音的发声
     *
     * @param chan  midi 通道
     * @param pitch 音高
     */
    public void noteoff(int chan, int pitch) {
        synth((byte) (0x80 | chan), (byte) pitch, (byte) 0);
    }

    /**
     * 停止指定 midi 通道的所以发声
     *
     * @param chan 如果通道是 -1 则停止所有通道的发声
     */
    public void mute(int chan) {
        if (synthPhy((byte) (0xb0 | chan), (byte) 0x78, (byte) 0)) return;
        if (_nativePtr == 0) return;
        nativeMute(_nativePtr, chan);
    }

    /**
     * 配置通道的音色
     *
     * @param chan       要配置的通道
     * @param bankNum    音色的 bank id
     * @param programNum 音色的 program id
     */
    public void selectProgram(int chan, int bankNum, int programNum) {
        // NOTE: 因为屋里设备和虚拟设备的音色库不同，所以这里没有向物理设备发送 midi 指令
        if (_nativePtr == 0) return;
        nativeSelectProgram(_nativePtr, chan, bankNum, programNum);
    }

    /**
     * 渲染原始的 midi event。如果不了解 midi event 结构，请不要直接使用这个接口
     */
    public void synth(final byte... event) {
        if (synthPhy(event)) return;
        if (_nativePtr == 0) return;
        nativeSynth(_nativePtr, event);
    }

    private static int getBestSampleRate(final Context context) {
        if (Build.VERSION.SDK_INT >= 17) {
            AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
            String sampleRateString = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
            return (sampleRateString == null) ? 44100 : Integer.parseInt(sampleRateString);
        } else {
            return 44100;
        }
    }

    private void initMidiDevice(final Context context) {
        _midiManager = (MidiManager) context.getSystemService(Context.MIDI_SERVICE);
        _midiManager.registerDeviceCallback(_midiDeviceCallback, null);

        MidiDeviceInfo[] infos = _midiManager.getDevices();
        if (infos != null) {
            Log.d(TAG, "发现已链接的 midi 设备数量为 " + infos.length);
            for (MidiDeviceInfo info : infos) {
                tryConnectMidiDevice(info);
            }
        } else {
            Log.d(TAG, "未发现已链接的 midi 设备");
        }
    }

    private synchronized void tryConnectMidiDevice(MidiDeviceInfo info) {
        if (info == null) return;
        Log.d(TAG, String.format("尝试链接设备，类型(%d 是 USB): %d, 设备ID: %d, iport 数量: %d, oport 数量: %d",
                MidiDeviceInfo.TYPE_USB, info.getType(), info.getId(),
                info.getInputPortCount(), info.getOutputPortCount()));
        if (_midiDevice != null) return;
        // 只支持 USB 链接，并且有 midi input port 的设备
        if (info.getType() != MidiDeviceInfo.TYPE_USB || info.getInputPortCount() == 0)
            return;
        // 打开设备
        _midiManager.openDevice(info, new OnDeviceOpenedListener() {
            @Override
            public void onDeviceOpened(MidiDevice midiDevice) {
                synchronized (this) {
                    if (midiDevice == null) {
                        Log.d(TAG, "打开 midi device 失败");
                        return;
                    } else {
                        Log.d(TAG, "打开 midi device " + midiDevice.getInfo().getId() + "成功: " + midiDevice.toString());
                        _midiDevice = midiDevice;
                        _midiInputPort = midiDevice.openInputPort(0);
                    }
                }
            }
        }, null);
    }

    private synchronized void tryDisconnectMidiDevice(MidiDeviceInfo info) {
        if (_midiDevice == null) return;
        if (info != null) {
            Log.d(TAG, "断开设备" + info.getId());
        } else {
            Log.d(TAG, "断开设备" + _midiDevice.getInfo().getId());
        }
        // info == null 表示强制关闭
        if (info != null && info.getId() != _midiDevice.getInfo().getId()) return;
        // close input port
        if (_midiInputPort != null) {
            try {
                _midiInputPort.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            _midiInputPort = null;
        }
        // close device
        try {
            _midiDevice.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        _midiDevice = null;
    }

    private synchronized boolean synthPhy(final byte... event) {
        if (_midiInputPort == null) return false;
        Log.d(TAG, "发送到物理设备的数据: " + Arrays.toString(event));
        try {
            _midiInputPort.send(event, 0, event.length, System.currentTimeMillis());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return true;
    }

    private static native long createNativePtr(AssetManager am, final String sf, int sampleRate, float gain);

    private static native void releaseNativePtr(long ptr);

    private static native void nativeSynth(long ptr, final byte event[]);

    private static native void nativeMute(long ptr, int chan);

    private static native void nativeSelectProgram(long ptr, int chan, int bank, int program);
}
